跳到主要内容

SpringSecurity JWT 模板

权限表设计

User 表 就是用户表

Role 表 就是角色表

User-Role表 用户角色表

Resource 表 资源表

Role-Resource 表 角色资源表

配置环境

<properties>
<java.version>1.8</java.version>
<jwt.version>0.10.7</jwt.version>
</properties>
<!-- ... -->

<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>

<!-- 引入 redis 当缓存服务器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 需要配置 redis 连接池,所以要引入这个包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.8.0</version>
</dependency>

<!-- 验证码工具 -->
<!-- 参考资料:https://juejin.im/post/6844903894661890055 -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>

<!-- API 文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<!--在引用时请在maven中央仓库搜索3.X最新版本号-->
<version>3.0.2</version>
</dependency>

编写一个常量类用来保存常量

public final class SecurityConstants {

/**
* 全局标识管理员
*/
public static final String ADMIN = "ROLE_ADMIN";

/**
* 设置验证码到期时间为 5 分钟
*/
public static final Integer IMAGE_CODE_EXPIRE_TIME = 5 * 60;

/**
* 验证码的 Key,加上这个可方便和其它 key 产生冲突
*/
public static final String IMAGE_CODE = "IMAGE_CODE";


/**
* 角色的 key
**/
public static final String ROLE_CLAIMS = "rol";

/**
* rememberMe 为 false 的时候过期时间是 1 个小时
*/
public static final long EXPIRATION = 60 * 60L;

/**
* rememberMe 为 true 的时候过期时间是7天
*/
public static final long EXPIRATION_REMEMBER = 60 * 60 * 24 * 7L;

/**
* JWT签名密钥
*/
public static final String JWT_SECRET_KEY = "pAjFyJV0w5BsxhjLcbeHPvif--pObLfqiEnQl1dhiQrNKRojMWA-5pGlndPwSo";

// JWT token defaults
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
public static final String TOKEN_TYPE = "JWT";

// Swagger WHITELIST
public static final String[] SWAGGER_WHITELIST = {
"/doc.html",
"/swagger-ui.html",
"/swagger-ui/*",
"/swagger-resources/**",
"/v2/api-docs",
"/v3/api-docs",
"/webjars/**"
};

/**
* 登录接口 WHITELIST 注意: 就算设置了 context-path 也不用加上 api
*/
public static final String AUTH_LOGIN_URL = "/auth/login";

/**
* 白名单,里面的请求直接放行
*/
public static final String[] GLOBAL_WHITE_LIST = {
"/utils/imagecode",
"/auth/logout"
};

// 过滤ALL
public static final String FILTER_ALL = "/**";

private SecurityConstants() {
}

}

编写 Result 响应模板

@Data
@Builder
public class Result<T> {
/**
* 业务码,比如成功、失败、权限不足等 code,可自行定义
*/
@ApiModelProperty("返回码")
private Integer code;
/**
* 返回信息,后端在进行业务处理后返回给前端一个提示信息,可自行定义
*/
@ApiModelProperty("返回信息")
private String message;
/**
* 数据结果,泛型,可以是列表、单个对象、数字、布尔值等
*/
@ApiModelProperty("返回数据")
private T data;
}

再创建一个工具类用来快速生成一个成功时的 Result

public final class ResultGeneratorUtils {

private ResultGeneratorUtils(){}

private static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";
private static final int RESULT_CODE_SUCCESS = 200;

public static Result<String> genSuccessResult() {
return Result.<String>builder()
.code(RESULT_CODE_SUCCESS)
.message(DEFAULT_SUCCESS_MESSAGE)
.build();
}

public static Result<String> genSuccessResult(String message) {
return Result.<String>builder()
.code(RESULT_CODE_SUCCESS)
.message(message)
.build();
}

public static <T> Result<T> genSuccessResult(T data) {
return Result.<T>builder()
.code(RESULT_CODE_SUCCESS)
.message(DEFAULT_SUCCESS_MESSAGE)
.data(data)
.build();
}

public static <T> Result<T> genSuccessResult(String message, T data) {
return Result.<T>builder()
.code(RESULT_CODE_SUCCESS)
.message(message)
.data(data)
.build();
}
}

Redis 配置

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {

// 配置缓存
@Bean
public RedisCacheConfiguration cacheConfiguration() {
// 设置缓存过期时间为 120 秒后
return RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(120)).disableCachingNullValues();
}

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 使用 RedisCacheManager 作为缓存管理器
return RedisCacheManager.builder(factory).cacheDefaults(cacheConfiguration()).transactionAware().build();
}

/**
* 默认情况下的模板只能支持 RedisTemplate<String, String> ,也就是只能存入字符串
* 这在开发中是不友好的,所以自定义模板是很有必要的,
* 当自定义了模板又想使用 String 存储这时候就可以使用 StringRedisTemplate 的方式和自定义模板共存的方式
*
* ConditionalOnMissingBean 注解作用在 @bean 定义上,它的作用就是在容器加载它作用的 bean 时,
* 检查容器中是否存在目标类型(ConditionalOnMissingBean 注解的 value 值)的 bean 了,
* 如果存在这跳过原始 bean 的 BeanDefinition 加载动作。
*/
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate( RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

登陆配置文件

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

private final StringRedisTemplate stringRedisTemplate;

/**
* 使用构造方法的形式自动注入 stringRedisTemplate
*
* @param stringRedisTemplate 把 JWT 存储到 Redis 里面
*/
public SecurityConfiguration(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}

/**
* 密码编码器
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}


@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors(withDefaults())
// 禁用 CSRF
.csrf().disable()
.authorizeRequests()
// 放行 swagger 文档
.antMatchers(SecurityConstants.SWAGGER_WHITELIST).permitAll()
// 放行登录接口
.antMatchers(HttpMethod.POST, SecurityConstants.AUTH_LOGIN_URL).permitAll()
// 设置白名单
.antMatchers(SecurityConstants.GLOBAL_WHITE_LIST).permitAll()
// 指定路径下的资源需要验证了的用户才能访问(这里默认是 ALL)
.antMatchers(SecurityConstants.FILTER_ALL).authenticated()
// 所有的删除操作必须是管理员才行
.antMatchers(HttpMethod.DELETE, SecurityConstants.FILTER_ALL).hasRole("ADMIN")
// 其它请求直接放行(通过 Spring EL 的注解在 Controller 上面写需要的权限,而非全部拦截)
.anyRequest().permitAll()
.and()
//添加自定义 Filter(这个只用来处理是否携带 Token,以及 Token 是否正确)
.addFilter(new JwtAuthorizationFilter(authenticationManager(), stringRedisTemplate))
// 不需要session(不创建会话,即不采用默认的 Session-Cookie 登陆策略)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 异常处理
.exceptionHandling()
// AccessDeniedHandler 仅适用于经过身份验证的用户。未经身份验证的用户的默认行为是重定向到登录页面,
// 如果要更改此设置,则需要配置一个 AuthenticationEntryPoint
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
// 因为使用的是自定义的登陆方式,所以需要自定义 403 处理
.accessDeniedHandler(new JwtAccessDeniedHandler());
// 防止 H2 web 页面的 Frame 被拦截
http.headers().frameOptions().disable();
}

/**
* Cors配置优化
**/
@Bean
CorsConfigurationSource corsConfigurationSource() {
org.springframework.web.cors.CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(singletonList("http://127.0.0.1:5500"));
configuration.setAllowedHeaders(singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "DELETE", "PUT", "OPTIONS"));
// 暴露这个 Authorization 属性,否则无法获取到 Token 信息
configuration.setExposedHeaders(singletonList(SecurityConstants.TOKEN_HEADER));
// 因为没有配置 HTTPS 所以就无需打开这个了
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600l);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

}

编写 JWT 工具类

@Slf4j
public final class JwtTokenUtils {

private JwtTokenUtils(){}

/**
* 生成足够的安全随机密钥,以适合符合规范的签名
*/
private static final byte[] API_KEY_SECRET_BYTES = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY);
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(API_KEY_SECRET_BYTES);

public static String createToken(String username, String id, List<String> roles, boolean isRememberMe) {
// 是否选择 isRememberMe 的过期时长不一样
long expiration = isRememberMe ? SecurityConstants.EXPIRATION_REMEMBER : SecurityConstants.EXPIRATION;

final Date createdDate = new Date();
final Date expirationDate = new Date(createdDate.getTime() + expiration * 1000);

String tokenPrefix = Jwts.builder()
.setHeaderParam("type", SecurityConstants.TOKEN_TYPE)
.signWith(SECRET_KEY, SignatureAlgorithm.HS256)
.claim(SecurityConstants.ROLE_CLAIMS, String.join(",", roles))
.setId(id)
.setIssuer("alsritter")
.setIssuedAt(createdDate)
.setSubject(username)
.setExpiration(expirationDate)
.compact();

return SecurityConstants.TOKEN_PREFIX + tokenPrefix; // 添加 token 前缀 "Bearer "; 这个是 OAuth 的协议
}

public static String getId(String token) {
Claims claims = getClaims(token);
return claims.getId();
}

/**
* @param token 传入一个 JWT
* @return 返回以一个
*/
public static UsernamePasswordAuthenticationToken getAuthentication(String token) {
// 先把 Token 解码成 Claims
Claims claims = getClaims(token);
List<SimpleGrantedAuthority> authorities = getAuthorities(claims);
String userName = claims.getSubject();
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}

private static List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
// 取得用户的权限
String role = (String) claims.get(SecurityConstants.ROLE_CLAIMS);
// GrantedAuthority 接口的默认实现 SimpleGrantedAuthority 中其实就只是比对字符串是否匹配
return Arrays.stream(role.split(",")) // 这个是流操作
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}

private static Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}

}

封装 UserDetails 对象

/**
* JWT用户对象
* @author alsritter
* @version 1.0
**/
public class JwtUser implements UserDetails {

private Integer id;
private String username;
private String password;
private Boolean enabled;
private Collection<? extends GrantedAuthority> authorities;

public JwtUser() {
}

/**
* 通过 user 对象创建jwtUser
*/
public JwtUser(TbUser user, Boolean enabled, Collection<? extends GrantedAuthority> authorities) {
this.id = user.getUserId();
this.username = user.getUserName();
this.password = user.getUserPassword();
this.enabled = enabled == null ? true : enabled;
this.authorities = authorities;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return this.enabled;
}

@Override
public String toString() {
return "JwtUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}

}

封装 RespBean

这里封装一个响应类,用来生成标准的 JSON 响应体

@AllArgsConstructor
@NoArgsConstructor
@Data
public class RespBean {
private Integer status; //状态码
private String msg; //返回信息
private Object obj; //数据

public static RespBean ok(String msg, Object obj) {
return new RespBean(200, msg, obj);
}

public static RespBean ok(String msg) {
return new RespBean(200, msg, null);
}

public static RespBean error(String msg, Object obj) {
return new RespBean(500, msg, obj);
}

public static RespBean error(String msg) {
return new RespBean(500, msg, null);
}

}

生成的 JSON 如下

{
"status": 200,
"msg": "登录成功!",
"obj": {
"password": null,
"username": "admin",
"authorities": [
{
"authority": "admin"
},
{
"authority": "normal"
}
],
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true
}
}

各种报错处理

RespBean respBean = null;
if (e instanceof BadCredentialsException ||
e instanceof UsernameNotFoundException) {
respBean = RespBean.error("账户名或者密码输入错误!");
} else if (e instanceof LockedException) {
respBean = RespBean.error("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean = RespBean.error("密码过期,请联系管理员!");
} else if (e instanceof AccountExpiredException) {
respBean = RespBean.error("账户过期,请联系管理员!");
} else if (e instanceof DisabledException) {
respBean = RespBean.error("账户被禁用,请联系管理员!");
} else {
respBean = RespBean.error("登录失败!");
}
resp.setStatus(401);
ObjectMapper om = new ObjectMapper();
PrintWriter out = resp.getWriter();
out.write(om.writeValueAsString(respBean));
out.flush();
out.close();

配置验证码

验证码样式

public class DisKaptchaCssImpl extends Configurable implements GimpyEngine {
Random rand = new Random();


@Override
public BufferedImage getDistortedImage(BufferedImage baseImage) {
NoiseProducer noiseProducer = this.getConfig().getNoiseImpl();
BufferedImage distortedImage = new BufferedImage(400, 125, 2);
Graphics2D graph = (Graphics2D) distortedImage.getGraphics();

RippleFilter rippleFilter = new RippleFilter();
rippleFilter.setXAmplitude(7.6F);
rippleFilter.setYAmplitude(rand.nextFloat() + 1.0F);
rippleFilter.setEdgeAction(1);
BufferedImage effectImage = rippleFilter.filter(baseImage, null);
graph.drawImage(effectImage, 0, 0, null, null);
graph.dispose();
noiseProducer.makeNoise(distortedImage, 0.1F, 0.1F, 0.25F, 0.25F);
noiseProducer.makeNoise(distortedImage, 0.1F, 0.25F, 0.5F, 0.9F);
return distortedImage;
}
}

验证码背景

public class NoKaptchaBackground extends Configurable implements BackgroundProducer {

@Override
public BufferedImage addBackground(BufferedImage baseImage) {
int width = 60;
int height = 30;
BufferedImage imageWithBackground = new BufferedImage(width, height, 1);
Graphics2D graph = (Graphics2D)imageWithBackground.getGraphics();
graph.fill(new Rectangle2D.Double(0.0D, 0.0D, (double)width, (double)height));
graph.drawImage(baseImage, 0, 0, null);
return imageWithBackground;
}
}

验证码配置类

@Configuration
public class KaptchaConfig {

/**
* =========Constant 描述 默认值==================
* kaptcha.border 图片边框,合法值:yes , no yes
* kaptcha.border.color 边框颜色,合法值: r,g,b (and optional alpha) 或者 white,black,blue. black
* kaptcha.border.thickness 边框厚度,合法值:>0 1
* kaptcha.image.width 图片宽 200
* kaptcha.image.height 图片高 50
* kaptcha.producer.impl 图片实现类 com.google.code.kaptcha.impl.DefaultKaptcha
* kaptcha.textproducer.impl 文本实现类 com.google.code.kaptcha.text.impl.DefaultTextCreator
* kaptcha.textproducer.char.string 文本集合,验证码值从此集合中获取 abcde2345678gfynmnpwx
* kaptcha.textproducer.char.length 验证码长度 5
* kaptcha.textproducer.font.names 字体 Arial, Courier
* kaptcha.textproducer.font.size 字体大小 40px.
* kaptcha.textproducer.font.color 字体颜色,合法值: r,g,b 或者 white,black,blue. black
* kaptcha.textproducer.char.space 文字间隔 2
* kaptcha.noise.impl 干扰实现类 com.google.code.kaptcha.impl.DefaultNoise
* kaptcha.noise.color 干扰颜色,合法值: r,g,b 或者 white,black,blue. black
* kaptcha.obscurificator.impl 图片样式:
* 水纹 com.google.code.kaptcha.impl.WaterRipple
* 鱼眼 com.google.code.kaptcha.impl.FishEyeGimpy
* 阴影 com.google.code.kaptcha.impl.ShadowGimpy com.google.code.kaptcha.impl.WaterRipple
* kaptcha.background.impl 背景实现类 com.google.code.kaptcha.impl.DefaultBackground
* kaptcha.background.clear.from 背景颜色渐变,开始颜色 light grey
* kaptcha.background.clear.to 背景颜色渐变,结束颜色 white
* kaptcha.word.impl 文字渲染器 com.google.code.kaptcha.text.impl.DefaultWordRenderer
* kaptcha.session.key session key KAPTCHA_SESSION_KEY
* kaptcha.session.date session date KAPTCHA_SESSION_DATE
*/


@Bean(name="captchaProducer")
public DefaultKaptcha getKaptchaBean(){
DefaultKaptcha defaultKaptcha=new DefaultKaptcha();
Properties properties=new Properties();
//验证码是否带边框 No
properties.setProperty("kaptcha.border", "no");
//验证码字体颜色
properties.setProperty("kaptcha.textproducer.font.color", "blue");
//验证码整体宽度(注意:因为自定义样式,所以需要再去修改 NoKaptchaBackhround 里的宽高)
properties.setProperty("kaptcha.image.width", "60");
//验证码整体高度
properties.setProperty("kaptcha.image.height", "30");
//文字个数
properties.setProperty("kaptcha.textproducer.char.length", "4");
//文字大小
properties.setProperty("kaptcha.textproducer.font.size","20");
//文字随机字体
properties.setProperty("kaptcha.textproducer.font.names", "宋体,楷体,微软雅黑");
//文字距离
properties.setProperty("kaptcha.textproducer.char.space","3");
//干扰线颜色
properties.setProperty("kaptcha.noise.color","blue");

// 这里添加自定义的样式
//自定义验证码样式
properties.setProperty("kaptcha.obscurificator.impl", DisKaptchaCssImpl.class.getName());
//自定义验证码背景
properties.setProperty("kaptcha.background.impl", NoKaptchaBackground.class.getName());
Config config=new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}

编写取得验证码的 API

@Api(tags = "常用的工具接口")
@Slf4j
@RestController
@RequestMapping("/utils")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UtilsController {

private final Producer captchaProducer;
private final StringRedisTemplate stringTemplate;

@ApiResponse(description = "返回一张验证码图片,这个接口无需权限")
@ApiOperation(value = "生成验证码图片", notes = "返回一张图片")
@GetMapping("/imagecode")
public void getImageCode(HttpServletResponse response, @RequestParam @ApiParam(value = "生成验证码的唯一标识符") String uuid) {
//禁止缓存
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
response.addHeader("Cache-Control", "post-check=0, pre-check=0");
response.setHeader("Pragma", "no-cache");

//设置响应格式为png图片
response.setContentType("image/png");

//为验证码创建一个文本
String codeText = captchaProducer.createText();
//将验证码存到redis
stringTemplate.opsForValue().set(SecurityConstants.IMAGE_CODE + uuid, codeText);
//设置验证码 5 分钟后到期
stringTemplate.expire(SecurityConstants.IMAGE_CODE + uuid, SecurityConstants.IMAGE_CODE_EXPIRE_TIME, TimeUnit.SECONDS);

try (ServletOutputStream out = response.getOutputStream()) {
// 用创建的验证码文本生成图片
BufferedImage bi = captchaProducer.createImage(codeText);
//写出图片
ImageIO.write(bi, "png", out);
out.flush();
} catch (Exception e) {
log.warn(e.getMessage());
throw new MyErrorException(ServiceErrorResultEnum.VERIFY_CODE_CREATE_ERROR);
}
}
}

登陆授权

验证身份 Controller

封装一个 LoginRequest 的 DTO

@Data
public class LoginRequest implements Serializable {
@NotEmpty(message = "验证码不能为空")
@ApiModelProperty(value = "验证码", required = true)
private String verify;
@NotEmpty(message = "唯一标识码不能为空")
@ApiModelProperty(value = "唯一标识码", required = true)
private String uuid;
@NotEmpty(message = "登录名不能为空")
@ApiModelProperty(value = "登录名", required = true)
private String username;
@NotEmpty(message = "密码不能为空")
@ApiModelProperty(value = "密码", required = true)
private String password;
@NotEmpty(message = "rememberMe 不能为空")
@ApiModelProperty(value = "记住我的选项", required = true)
private Boolean rememberMe;
}

编写具体的验证 Controller

@Slf4j
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Api(tags = "认证")
public class AuthController {

private final AuthService authService;
private final StringRedisTemplate stringTemplate;

@PostMapping("/login")
@ApiOperation("登录")
public ResponseEntity<Result<String>> login(@ApiParam("登陆的参数") @RequestBody LoginRequest loginRequest) {
String verifyCode = loginRequest.getVerify();
String uuid = loginRequest.getUuid();
String previousVCode = stringTemplate.opsForValue().get(SecurityConstants.IMAGE_CODE + uuid);

// 先验证这个 uuid 是否存在
if (previousVCode == null) {
throw new MyWarnException(ServiceErrorResultEnum.VERIFY_CODE_TIMEOUT_OR_NOT_EXIST);
}

// 再判断验证码是否正确
if (!previousVCode.trim().equals(verifyCode)) {
throw new MyWarnException(ServiceErrorResultEnum.VERIFY_CODE_CREATE_ERROR);
}

String token = authService.getToken(loginRequest);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(SecurityConstants.TOKEN_HEADER, token);
return ResponseEntity.ok().headers(httpHeaders).body(ResultGeneratorUtils.genSuccessResult());
}

@PostMapping("/logout")
@ApiOperation("退出登陆")
public ResponseEntity<Result<String>> logout() {
authService.deleteTokenFromRedis();
return ResponseEntity.ok().body(ResultGeneratorUtils.genSuccessResult());
}
}

AuthService

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthServiceImpl implements AuthService {
private final UserService userService;
private final UserRoleService userRoleService;
private final StringRedisTemplate stringRedisTemplate;
private final CurrentUserUtils currentUserUtils;

@Override
public String getToken(LoginRequest loginRequest) {
TbUser user = userService.find(loginRequest.getUsername());
if (!userService.check(loginRequest.getPassword(), user.getUserPassword())) {
throw new MyWarnException(ServiceErrorResultEnum.LOGIN_ERROR);
}

// 取得该用户的权限
List<TbRoles> permissionList = userRoleService.getPermissionList(user.getUserId());
JwtUser jwtUser = new JwtUser(user, loginRequest.getRememberMe(), permissionList);

// 获取权限列表
List<String> authorities = jwtUser.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList());


String token = JwtTokenUtils.createToken(
user.getUserName(),
user.getUserId().toString(),
authorities,
loginRequest.getRememberMe());
// 存到 redis 里面先
stringRedisTemplate.opsForValue().set(user.getUserId().toString(), token);
return token;
}

@Override
public void deleteTokenFromRedis() {
stringRedisTemplate.delete(currentUserUtils.getCurrentUser().getUserId().toString());
}
}

UserService

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements UserService {
private final TbUserMapper userMapper;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

@Override
public TbUser find(String userName) {
TbUser tbUser = userMapper.selectByUserNameTbUser(userName);
if (tbUser == null) {
throw new MyWarnException(ServiceErrorResultEnum.NOT_FOUND);
}

return tbUser;
}

@Override
public boolean check(String currentPassword, String password) {
return this.bCryptPasswordEncoder.matches(currentPassword,password);
}
}

授权检查

用户请求 Token 过滤器

/**
* 验证 JWT 信息的过滤器,用于判断权限
* 过滤器处理所有 HTTP 请求,并检查是否存在带有正确令牌的 Authorization 标头。
* 例如,如果令牌未过期或签名密钥正确。
*
* SpringSecurity 提供两种登陆方式
* `UsernamePasswordAuthenticationFilter` 表示表单登陆过滤器
* `BasicAuthenticationFilter` 表示 httpBasic 方式登陆过滤器
*
* @author alsritter
* @version 1.0
**/
@Slf4j
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

private final StringRedisTemplate stringRedisTemplate;

public JwtAuthorizationFilter(AuthenticationManager authenticationManager, StringRedisTemplate stringRedisTemplate) {
super(authenticationManager);
this.stringRedisTemplate = stringRedisTemplate;
}

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {

String token = request.getHeader(SecurityConstants.TOKEN_HEADER);
// token 为空,或者不规范(不以 Bearer 开头)的先清除上下文,使之直接跳转到登陆页
if (token == null || !token.startsWith(SecurityConstants.TOKEN_PREFIX)) {
SecurityContextHolder.clearContext();
chain.doFilter(request, response);
return;
}

String tokenValue = token.replace(SecurityConstants.TOKEN_PREFIX, "");
// 这个 UsernamePasswordAuthenticationToken 用来存储
// 关键是能直接在这里通过 getPrincipal() 取得 UserDetails(虽然显示是 Object,但是实际是 UserDetails)
//
UsernamePasswordAuthenticationToken authentication = null;
try {
String previousToken = stringRedisTemplate.opsForValue().get(JwtTokenUtils.getId(tokenValue));
// 和存储在 Redis 里的 Token 进行比对,看是否过期了
if (!token.equals(previousToken)) {
SecurityContextHolder.clearContext();
chain.doFilter(request, response);
return;
}

// 这里也封装了权限信息
authentication = JwtTokenUtils.getAuthentication(tokenValue);
} catch (JwtException e) {
logger.error("Invalid jwt : " + e.getMessage());
}

// 全部认证通过就可以把这个存储了个人信息的 authentication 传递到 Context 中,以便后面调用
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}
}

取得当前用户工具类

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CurrentUserUtils {

private final UserService userService;

public TbUser getCurrentUser() {
return userService.find(getCurrentUserName());
}

private String getCurrentUserName() {
// 直接从线程的上下文中取得这个用户的信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() != null) {
return (String) authentication.getPrincipal();
}
// 找不到则抛出一个要求登陆的异常,因为需要调用这个方法的地方基本都是要求登陆的且都能到这里表示还是携带了 Token 的,所以这里姑且认为用户是过期了
throw new AccountExpiredException("找不到用户信息");
}

public Collection<? extends GrantedAuthority> getAuthorities() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() != null) {
return authentication.getAuthorities();
}
// 没找到则返回一个空集
throw new AccountExpiredException("找不到用户信息");
}
}

编写测试授权 API

@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@RequestMapping("/temp")
@Api(tags = "测试 API")
public class TempController {

private final CurrentUserUtils currentUserUtils;

@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
@ApiOperation("说你好,需要 ADMIN 权限")
@GetMapping("/hello")
public ResponseEntity<Result<Collection<? extends GrantedAuthority>>> sayHello() {
return ResponseEntity
.ok()
.body(ResultGeneratorUtils.genSuccessResult(currentUserUtils.getAuthorities()));
}
}

编写错误拦截器

用户访问无权限时异常

/**
* 用来解决认证过的用户访问无权限资源时的异常
* 注意:
* AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
* AccessDeniedHandler 用来解决认证过的用户访问无权限资源时的异常
*
* @author alsritter
* @version 1.0
**/
@Slf4j
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
/**
* 当用户尝试访问需要权限才能的 REST 资源而权限不足的时候,
* 将调用此方法发送 403 响应以及错误信息
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
PrintWriter out = null;
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(Result.<String>builder()
.code(ServiceErrorResultEnum.NOT_ENOUGH_PERMISSIONS.getResultCode())
.message(ServiceErrorResultEnum.NOT_ENOUGH_PERMISSIONS.getResultMsg())
.build());

try {
response.setStatus(ServiceErrorResultEnum.NOT_ENOUGH_PERMISSIONS.getResultCode());
response.setContentType("application/json;charset=utf-8");
out = response.getWriter();
out.println(json);
} finally {
if (null != out) {
out.flush();
out.close();
}
}
log.warn(accessDeniedException.getMessage());
}
}

匿名访问异常

/**
* 用来解决匿名用户(就是未登录的)访问需要权限才能访问的资源时的异常
* 注意:
* AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
* AccessDeniedHandler 用来解决认证过的用户访问无权限资源时的异常
*
* @author alsritter
* @version 1.0
**/
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

/**
* 当用户尝试访问需要权限才能的 REST 资源而不提供 Token 或者 Token 错误或者过期时,
* 将调用此方法发送 401 响应以及错误信息
*
* @param request 遇到了认证异常 authException 用户请求
* @param response 是将要返回给客户的响应,方法 commence 实现,也就是相应的认证方案逻辑会修改 response 并返回给用户引导用户进入认证流程。
* @param authException 具体认证的错误
* @throws IOException 发送过程的错误
*/
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.warn(authException.getMessage());

ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(Result.<String>builder()
.code(ServiceErrorResultEnum.NOT_LOGIN_ERROR.getResultCode())
.message(ServiceErrorResultEnum.NOT_LOGIN_ERROR.getResultMsg())
.build());


PrintWriter out = null;
try {
response.setStatus(ServiceErrorResultEnum.NOT_LOGIN_ERROR.getResultCode());
response.setContentType("application/json;charset=utf-8");
out = response.getWriter();
out.println(json);
} finally {
if (null != out) {
out.flush();
out.close();
}
}
}
}

全局异常处理

@ControllerAdvice
@Slf4j
@ResponseBody
public class GlobalExceptionHandler {

/**
* 处理自定义的业务异常
*
* @param e Spring 会捕获异常传入这个方法里
*/
@ExceptionHandler(value = MyErrorException.class)
public ResponseEntity<Result<String>> bizExceptionHandler(HttpServletResponse response, MyErrorException e) throws IOException {
log.error("发生业务异常!原因是:{}", e.getErrorMsg());
return ResponseEntity.status(e.getErrorCode()).body(
Result.<String>builder()
.code(e.getErrorCode())
.message(e.getErrorMsg())
.build()
);
}

/**
* 处理自定义的业务异常
*
* @param e Spring 会捕获异常传入这个方法里
*/
@ExceptionHandler(value = MyWarnException.class)
public ResponseEntity<Result<String>> bizExceptionHandler(HttpServletResponse response, MyWarnException e) throws IOException {
log.warn("发生业务异常!原因是:{}", e.getErrorMsg());
return ResponseEntity.status(e.getErrorCode()).body(
Result.<String>builder()
.code(e.getErrorCode())
.message(e.getErrorMsg())
.build()
);
}

/**
* 处理空指针的异常
*
* @param e Spring 会捕获 BizException 异常传入这个方法里
*/
@ExceptionHandler(value = NullPointerException.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, NullPointerException e) throws IOException {
log.error("发生空指针异常!原因是: ", e);
return ResponseEntity.status(ServiceErrorResultEnum.INTERNAL_SERVER_ERROR.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.INTERNAL_SERVER_ERROR.getResultCode())
.message(ServiceErrorResultEnum.INTERNAL_SERVER_ERROR.getResultMsg())
.build()
);
}

/**
* api 请求类型不符异常
*/
@ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, HttpRequestMethodNotSupportedException e) throws IOException {
log.warn("api 请求类型不符合 当前请求的方法是: {}", e.getMethod());
return ResponseEntity.status(ServiceErrorResultEnum.REQUEST_METHOD_NOT_EXIST.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.REQUEST_METHOD_NOT_EXIST.getResultCode())
.message(ServiceErrorResultEnum.REQUEST_METHOD_NOT_EXIST.getResultMsg())
.build()
);
}

/**
* 参数读取异常
*/
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, HttpMessageNotReadableException e) throws IOException {
log.warn("参数读取异常 HTTP 请求的是: {}", e.getHttpInputMessage());
return ResponseEntity.status(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultCode())
.message(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultMsg())
.build()
);
}// MissingServletRequestParameterException

/**
* 丢失参数异常
*/
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, MissingServletRequestParameterException e) throws IOException {
log.warn("丢失参数: {}", e.getParameterName());
return ResponseEntity.status(ServiceErrorResultEnum.MISSING_PARAMETER_ERROR.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.MISSING_PARAMETER_ERROR.getResultCode())
.message(ServiceErrorResultEnum.MISSING_PARAMETER_ERROR.getResultMsg())
.build()
);
}// MissingServletRequestParameterException

/**
* 请求参数绑定到 java bean 上失败时抛出
*/
@ExceptionHandler(value = BindException.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, BindException e) throws IOException {
log.error("参数绑定到 Bean 上异常,来源于:{} 要绑定的属性名是:{}", e.getOrigin(), e.getProperty());
return ResponseEntity.status(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultCode())
.message(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultMsg())
.build()
);
}

/**
* 参数转换错误,因为 Spring 会自动把前端的请求参数转成对应的数据返回到形参上,所以当前端传入错误的参数过来转换不了就会报这个错
*/
@ExceptionHandler(value = MethodArgumentTypeMismatchException.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, MethodArgumentTypeMismatchException e) throws IOException {
log.error("参数转换错误,可能是传入的参数名和 value 位置写反了 访问的方法是:{} 参数名称为:{}", e.getParameter(), e.getName());
return ResponseEntity.status(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultCode())
.message(ServiceErrorResultEnum.PARAMETER_NOT_READABLE.getResultMsg())
.build()
);
}

/**
* 处理其他异常
*
* @param e Spring 会捕获 BizException 异常传入这个方法里
*/
@ExceptionHandler(value = Exception.class)
public ResponseEntity<Result<String>> exceptionHandler(HttpServletResponse response, Exception e) throws Exception {
if (e instanceof AccessDeniedException
|| e instanceof AuthenticationException) {
throw e;
}
log.error("未知异常!原因是:", e);
return ResponseEntity.status(ServiceErrorResultEnum.INTERNAL_SERVER_ERROR.getResultCode()).body(
Result.<String>builder()
.code(ServiceErrorResultEnum.INTERNAL_SERVER_ERROR.getResultCode())
.message(ServiceErrorResultEnum.INTERNAL_SERVER_ERROR.getResultMsg())
.build()
);
}
}